/*
 * Copyright 2004-2014 SmartBear Software
 *
 * Licensed under the EUPL, Version 1.1 or - as soon as they will be approved by the European Commission - subsequent
 * versions of the EUPL (the "Licence");
 * You may not use this work except in compliance with the Licence.
 * You may obtain a copy of the Licence at:
 *
 * http://ec.europa.eu/idabc/eupl
 *
 * Unless required by applicable law or agreed to in writing, software distributed under the Licence is
 * distributed on an "AS IS" basis, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
 * express or implied. See the Licence for the specific language governing permissions and limitations
 * under the Licence.
*/

package com.eviware.soapui.impl.wsdl.support.xsd;

import java.io.File;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;

import javax.xml.namespace.QName;

import org.apache.log4j.Logger;
import org.apache.xmlbeans.SchemaAnnotation;
import org.apache.xmlbeans.SchemaField;
import org.apache.xmlbeans.SchemaLocalElement;
import org.apache.xmlbeans.SchemaType;
import org.apache.xmlbeans.SchemaTypeSystem;
import org.apache.xmlbeans.SimpleValue;
import org.apache.xmlbeans.XmlAnySimpleType;
import org.apache.xmlbeans.XmlBase64Binary;
import org.apache.xmlbeans.XmlBeans;
import org.apache.xmlbeans.XmlCursor;
import org.apache.xmlbeans.XmlException;
import org.apache.xmlbeans.XmlHexBinary;
import org.apache.xmlbeans.XmlObject;
import org.apache.xmlbeans.XmlOptions;
import org.w3c.dom.Attr;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;

import com.eviware.soapui.SoapUI;
import com.eviware.soapui.SoapUIExtensionClassLoader;
import com.eviware.soapui.SoapUIExtensionClassLoader.SoapUIClassLoaderState;
import com.eviware.soapui.impl.wsdl.support.Constants;
import com.eviware.soapui.model.settings.SettingsListener;
import com.eviware.soapui.settings.WsdlSettings;
import com.eviware.soapui.support.StringUtils;
import com.eviware.soapui.support.Tools;
import com.eviware.soapui.support.xml.XmlUtils;

/**
 * XML-Schema related tools
 *
 * @author Ole.Matzura
 */

public class SchemaUtils {
    private final static Logger log = Logger.getLogger(SchemaUtils.class);
    private static Map<String, XmlObject> defaultSchemas = new HashMap<String, XmlObject>();

    static {
        initDefaultSchemas();

        SoapUI.getSettings().addSettingsListener(new SettingsListener() {

            public void settingChanged(String name, String newValue, String oldValue) {
                if (name.equals(WsdlSettings.SCHEMA_DIRECTORY)) {
                    log.info("Reloading default schemas..");
                    initDefaultSchemas();
                }
            }

            @Override
            public void settingsReloaded() {
                // TODO Auto-generated method stub

            }
        });
    }

    public static void initDefaultSchemas() {
        SoapUIClassLoaderState state = SoapUIExtensionClassLoader.ensure();

        try {
            defaultSchemas.clear();

            String root = "/com/eviware/soapui/resources/xsds";

            loadDefaultSchema(SoapUI.class.getResource(root + "/xop.xsd"));
            loadDefaultSchema(SoapUI.class.getResource(root + "/XMLSchema.xsd"));
            loadDefaultSchema(SoapUI.class.getResource(root + "/xml.xsd"));
            loadDefaultSchema(SoapUI.class.getResource(root + "/swaref.xsd"));
            loadDefaultSchema(SoapUI.class.getResource(root + "/xmime200505.xsd"));
            loadDefaultSchema(SoapUI.class.getResource(root + "/xmime200411.xsd"));
            loadDefaultSchema(SoapUI.class.getResource(root + "/soapEnvelope.xsd"));
            loadDefaultSchema(SoapUI.class.getResource(root + "/soapEncoding.xsd"));
            loadDefaultSchema(SoapUI.class.getResource(root + "/soapEnvelope12.xsd"));
            loadDefaultSchema(SoapUI.class.getResource(root + "/soapEncoding12.xsd"));

            String schemaDirectory = SoapUI.getSettings().getString(WsdlSettings.SCHEMA_DIRECTORY, null);
            if (StringUtils.hasContent(schemaDirectory)) {
                loadSchemaDirectory(schemaDirectory);
            }
        } catch (Exception e) {
            SoapUI.logError(e);
        } finally {
            state.restore();
        }
    }

    private static void loadSchemaDirectory(String schemaDirectory) throws IOException, MalformedURLException {
        File dir = new File(schemaDirectory);
        if (dir.exists() && dir.isDirectory()) {
            String[] xsdFiles = dir.list();
            int cnt = 0;

            if (xsdFiles != null && xsdFiles.length > 0) {
                for (int c = 0; c < xsdFiles.length; c++) {
                    try {
                        String xsdFile = xsdFiles[c];
                        if (xsdFile.endsWith(".xsd")) {
                            String filename = schemaDirectory + File.separator + xsdFile;
                            loadDefaultSchema(new URL("file:" + filename));
                            cnt++;
                        }
                    } catch (Throwable e) {
                        SoapUI.logError(e);
                    }
                }
            }

            if (cnt == 0) {
                log.warn("Missing schema files in  schemaDirectory [" + schemaDirectory + "]");
            }
        } else {
            log.warn("Failed to open schemaDirectory [" + schemaDirectory + "]");
        }
    }

    private static void loadDefaultSchema(URL url) throws Exception {
        // XmlObject xmlObject = XmlObject.Factory.parse( url );
        XmlObject xmlObject = XmlUtils.createXmlObject(url);
        if (!((Document) xmlObject.getDomNode()).getDocumentElement().getNamespaceURI().equals(Constants.XSD_NS)) {
            return;
        }

        String targetNamespace = getTargetNamespace(xmlObject);

        if (defaultSchemas.containsKey(targetNamespace)) {
            log.warn("Overriding schema for targetNamespace " + targetNamespace);
        }

        defaultSchemas.put(targetNamespace, xmlObject);

        log.info("Added default schema from " + url.getPath() + " with targetNamespace " + targetNamespace);
    }

    public static SchemaTypeSystem loadSchemaTypes(String wsdlUrl, SchemaLoader loader) throws SchemaException {
        SoapUIClassLoaderState state = SoapUIExtensionClassLoader.ensure();

        try {
            log.info("Loading schema types from [" + wsdlUrl + "]");
            ArrayList<XmlObject> schemas = new ArrayList<XmlObject>(getSchemas(wsdlUrl, loader).values());

            return buildSchemaTypes(schemas);
        } catch (Exception e) {
            SoapUI.logError(e);
            if (e instanceof SchemaException) {
                throw (SchemaException) e;
            } else {
                throw new SchemaException("Error loading schema types", e);
            }
        } finally {
            state.restore();
        }
    }

    public static SchemaTypeSystem buildSchemaTypes(List<XmlObject> schemas) throws SchemaException {
        XmlOptions options = new XmlOptions();
        options.setCompileNoValidation();
        options.setCompileNoPvrRule();
        options.setCompileDownloadUrls();
        options.setCompileNoUpaRule();
        options.setValidateTreatLaxAsSkip();

        for (int c = 0; c < schemas.size(); c++) {
            XmlObject xmlObject = schemas.get(c);
            if (xmlObject == null
                    || !((Document) xmlObject.getDomNode()).getDocumentElement().getNamespaceURI()
                    .equals(Constants.XSD_NS)) {
                schemas.remove(c);
                c--;
            }
        }

        boolean strictSchemaTypes = SoapUI.getSettings().getBoolean(WsdlSettings.STRICT_SCHEMA_TYPES);
        if (!strictSchemaTypes) {
            Set<String> mdefNamespaces = new HashSet<String>();

            for (XmlObject xObj : schemas) {
                mdefNamespaces.add(getTargetNamespace(xObj));
            }

            options.setCompileMdefNamespaces(mdefNamespaces);
        }

        ArrayList<?> errorList = new ArrayList<Object>();
        options.setErrorListener(errorList);

        XmlCursor cursor = null;

        try {
            // remove imports
            for (int c = 0; c < schemas.size(); c++) {
                XmlObject s = schemas.get(c);

                Map<?, ?> map = new HashMap<String, String>();
                cursor = s.newCursor();
                cursor.toStartDoc();
                if (toNextContainer(cursor)) {
                    cursor.getAllNamespaces(map);
                } else {
                    log.warn("Can not get namespaces for " + s);
                }

                String tns = getTargetNamespace(s);

                // log.info( "schema for [" + tns + "] contained [" + map.toString()
                // + "] namespaces" );

                if (strictSchemaTypes && defaultSchemas.containsKey(tns)) {
                    schemas.remove(c);
                    c--;
                } else {
                    removeImports(s);
                }

                cursor.dispose();
                cursor = null;
            }

            // schemas.add( soapVersion.getSoapEncodingSchema());
            // schemas.add( soapVersion.getSoapEnvelopeSchema());
            schemas.addAll(defaultSchemas.values());

            SchemaTypeSystem sts = XmlBeans.compileXsd(schemas.toArray(new XmlObject[schemas.size()]),
                    XmlBeans.getBuiltinTypeSystem(), options);

            return sts;
            // return XmlBeans.typeLoaderUnion(new SchemaTypeLoader[] { sts,
            // XmlBeans.getBuiltinTypeSystem() });
        } catch (Exception e) {
            SoapUI.logError(e);
            throw new SchemaException(e, errorList);
        } finally {
            for (int c = 0; c < errorList.size(); c++) {
                log.warn("Error: " + errorList.get(c));
            }

            if (cursor != null) {
                cursor.dispose();
            }
        }
    }

    public static boolean toNextContainer(XmlCursor cursor) {
        while (!cursor.isContainer() && !cursor.isEnddoc()) {
            cursor.toNextToken();
        }

        return cursor.isContainer();
    }

    public static String getTargetNamespace(XmlObject s) {
        return ((Document) s.getDomNode()).getDocumentElement().getAttribute("targetNamespace");
    }

    public static Map<String, XmlObject> getSchemas(String wsdlUrl, SchemaLoader loader) throws SchemaException {
        Map<String, XmlObject> result = new HashMap<String, XmlObject>();
        getSchemas(wsdlUrl, result, loader, null /* , false */);
        return result;
    }

    /**
     * Returns a map mapping urls to corresponding XmlSchema XmlObjects for the
     * specified wsdlUrl
     */

    public static void getSchemas(String wsdlUrl, Map<String, XmlObject> existing, SchemaLoader loader, String tns)
            throws SchemaException {

        if (existing.containsKey(wsdlUrl)) {
            return;
        }

        // if( add )
        // existing.put( wsdlUrl, null );

        log.info("Getting schema " + wsdlUrl);

        ArrayList<?> errorList = new ArrayList<Object>();

        Map<String, XmlObject> result = new HashMap<String, XmlObject>();

        boolean common = false;

        try {
            XmlOptions options = new XmlOptions();
            options.setCompileNoValidation();
            options.setSaveUseOpenFrag();
            options.setErrorListener(errorList);
            options.setSaveSyntheticDocumentElement(new QName(Constants.XSD_NS, "schema"));

            XmlObject xmlObject = loader.loadXmlObject(wsdlUrl, options);
            if (xmlObject == null) {
                throw new Exception("Failed to load schema from [" + wsdlUrl + "]");
            }

            Document dom = (Document) xmlObject.getDomNode();
            Node domNode = dom.getDocumentElement();

            // is this an xml schema?
            if (domNode.getLocalName().equals("schema") && Constants.XSD_NS.equals(domNode.getNamespaceURI())) {
                // set targetNamespace (this happens if we are following an include
                // statement)
                if (tns != null) {
                    Element elm = ((Element) domNode);
                    if (!elm.hasAttribute("targetNamespace")) {
                        common = true;
                        elm.setAttribute("targetNamespace", tns);
                    }

                    // check for namespace prefix for targetNamespace
                    NamedNodeMap attributes = elm.getAttributes();
                    int c = 0;
                    for (; c < attributes.getLength(); c++) {
                        Node item = attributes.item(c);
                        if (item.getNodeName().equals("xmlns")) {
                            break;
                        }

                        if (item.getNodeValue().equals(tns) && item.getNodeName().startsWith("xmlns")) {
                            break;
                        }
                    }

                    if (c == attributes.getLength()) {
                        elm.setAttribute("xmlns", tns);
                    }
                }

                if (common && !existing.containsKey(wsdlUrl + "@" + tns)) {
                    result.put(wsdlUrl + "@" + tns, xmlObject);
                } else {
                    result.put(wsdlUrl, xmlObject);
                }
            } else {
                existing.put(wsdlUrl, null);

                XmlObject[] paths = xmlObject.selectPath("declare namespace s='" + Constants.XSD_NS + "' .//s:schema");

                for (int i = 0; i < paths.length; i++) {
                    XmlCursor xmlCursor = paths[i].newCursor();
                    String xmlText = xmlCursor.getObject().xmlText(options);

                    XmlObject obj = XmlUtils.createXmlObject(xmlText, new XmlOptions());
                    obj.documentProperties().setSourceName(wsdlUrl);

                    result.put(wsdlUrl + "@" + (i + 1), obj);
                }

                XmlObject[] wsdlImports = xmlObject.selectPath("declare namespace s='" + Constants.WSDL11_NS
                        + "' .//s:import/@location");
                for (int i = 0; i < wsdlImports.length; i++) {
                    String location = ((SimpleValue) wsdlImports[i]).getStringValue();
                    if (location != null) {
                        if (!location.startsWith("file:") && location.indexOf("://") == -1) {
                            location = Tools.joinRelativeUrl(wsdlUrl, location);
                        }

                        getSchemas(location, existing, loader, null);
                    }
                }

                XmlObject[] wadl10Imports = xmlObject.selectPath("declare namespace s='" + Constants.WADL10_NS
                        + "' .//s:grammars/s:include/@href");
                for (int i = 0; i < wadl10Imports.length; i++) {
                    String location = ((SimpleValue) wadl10Imports[i]).getStringValue();
                    if (location != null) {
                        if (!location.startsWith("file:") && location.indexOf("://") == -1) {
                            location = Tools.joinRelativeUrl(wsdlUrl, location);
                        }

                        getSchemas(location, existing, loader, null);
                    }
                }

                XmlObject[] wadlImports = xmlObject.selectPath("declare namespace s='" + Constants.WADL11_NS
                        + "' .//s:grammars/s:include/@href");
                for (int i = 0; i < wadlImports.length; i++) {
                    String location = ((SimpleValue) wadlImports[i]).getStringValue();
                    if (location != null) {
                        if (!location.startsWith("file:") && location.indexOf("://") == -1) {
                            location = Tools.joinRelativeUrl(wsdlUrl, location);
                        }

                        getSchemas(location, existing, loader, null);
                    }
                }

            }

            existing.putAll(result);

            XmlObject[] schemas = result.values().toArray(new XmlObject[result.size()]);

            for (int c = 0; c < schemas.length; c++) {
                xmlObject = schemas[c];

                XmlObject[] schemaImports = xmlObject.selectPath("declare namespace s='" + Constants.XSD_NS
                        + "' .//s:import/@schemaLocation");
                for (int i = 0; i < schemaImports.length; i++) {
                    String location = ((SimpleValue) schemaImports[i]).getStringValue();
                    Element elm = ((Attr) schemaImports[i].getDomNode()).getOwnerElement();

                    if (location != null && !defaultSchemas.containsKey(elm.getAttribute("namespace"))) {
                        if (!location.startsWith("file:") && location.indexOf("://") == -1) {
                            location = Tools.joinRelativeUrl(wsdlUrl, location);
                        }

                        getSchemas(location, existing, loader, null);
                    }
                }

                XmlObject[] schemaIncludes = xmlObject.selectPath("declare namespace s='" + Constants.XSD_NS
                        + "' .//s:include/@schemaLocation");
                for (int i = 0; i < schemaIncludes.length; i++) {
                    String location = ((SimpleValue) schemaIncludes[i]).getStringValue();
                    if (location != null) {
                        String targetNS = getTargetNamespace(xmlObject);

                        if (!location.startsWith("file:") && location.indexOf("://") == -1) {
                            location = Tools.joinRelativeUrl(wsdlUrl, location);
                        }

                        getSchemas(location, existing, loader, targetNS);
                    }
                }
            }
        } catch (Exception e) {
            SoapUI.logError(e);
            throw new SchemaException(e, errorList);
        }
    }

    /**
     * Returns a map mapping urls to corresponding XmlObjects for the specified
     * wsdlUrl
     */

    public static Map<String, XmlObject> getDefinitionParts(SchemaLoader loader) throws Exception {
        Map<String, XmlObject> result = new LinkedHashMap<String, XmlObject>();
        getDefinitionParts(loader.getBaseURI(), result, loader);
        return result;
    }

    public static void getDefinitionParts(String origWsdlUrl, Map<String, XmlObject> existing, SchemaLoader loader)
            throws Exception {
        String wsdlUrl = origWsdlUrl;
        if (existing.containsKey(wsdlUrl)) {
            return;
        }

        XmlObject xmlObject = loader.loadXmlObject(wsdlUrl, null);
        existing.put(wsdlUrl, xmlObject);
        // wsdlUrl = loader.getBaseURI();

        selectDefinitionParts(wsdlUrl, existing, loader, xmlObject, "declare namespace s='" + Constants.WSDL11_NS
                + "' .//s:import/@location");
        selectDefinitionParts(wsdlUrl, existing, loader, xmlObject, "declare namespace s='" + Constants.WADL10_NS
                + "' .//s:grammars/s:include/@href");
        selectDefinitionParts(wsdlUrl, existing, loader, xmlObject, "declare namespace s='" + Constants.WADL11_NS
                + "' .//s:grammars/s:include/@href");
        selectDefinitionParts(wsdlUrl, existing, loader, xmlObject, "declare namespace s='" + Constants.XSD_NS
                + "' .//s:import/@schemaLocation");
        selectDefinitionParts(wsdlUrl, existing, loader, xmlObject, "declare namespace s='" + Constants.XSD_NS
                + "' .//s:include/@schemaLocation");
    }

    private static void selectDefinitionParts(String wsdlUrl, Map<String, XmlObject> existing, SchemaLoader loader,
                                              XmlObject xmlObject, String path) throws Exception {
        XmlObject[] wsdlImports = xmlObject.selectPath(path);
        for (int i = 0; i < wsdlImports.length; i++) {
            String location = ((SimpleValue) wsdlImports[i]).getStringValue();
            if (location != null) {
                if (StringUtils.hasContent(location)) {
                    if (!location.startsWith("file:") && location.indexOf("://") == -1) {
                        location = Tools.joinRelativeUrl(wsdlUrl, location);
                    }

                    getDefinitionParts(location, existing, loader);
                } else {
                    Node domNode = ((Attr) wsdlImports[i].getDomNode()).getOwnerElement();
                    domNode.getParentNode().removeChild(domNode);
                }
            }
        }
    }

    /**
     * Extracts namespaces - used in tool integrations for mapping..
     */

    public static Collection<String> extractNamespaces(SchemaTypeSystem schemaTypes, boolean removeDefault) {
        Set<String> namespaces = new HashSet<String>();
        SchemaType[] globalTypes = schemaTypes.globalTypes();
        for (int c = 0; c < globalTypes.length; c++) {
            namespaces.add(globalTypes[c].getName().getNamespaceURI());
        }

        if (removeDefault) {
            namespaces.removeAll(defaultSchemas.keySet());
            namespaces.remove(Constants.SOAP11_ENVELOPE_NS);
            namespaces.remove(Constants.SOAP_ENCODING_NS);
        }

        return namespaces;
    }

    /**
     * Used when creating a TypeSystem from a complete collection of
     * SchemaDocuments so that referenced types are not downloaded (again)
     */

    public static void removeImports(XmlObject xmlObject) throws XmlException {
        XmlObject[] imports = xmlObject.selectPath("declare namespace s='" + Constants.XSD_NS + "' .//s:import");

        for (int c = 0; c < imports.length; c++) {
            XmlCursor cursor = imports[c].newCursor();
            cursor.removeXml();
            cursor.dispose();
        }

        XmlObject[] includes = xmlObject.selectPath("declare namespace s='" + Constants.XSD_NS + "' .//s:include");

        for (int c = 0; c < includes.length; c++) {
            XmlCursor cursor = includes[c].newCursor();
            cursor.removeXml();
            cursor.dispose();
        }
    }

    public static boolean isInstanceOf(SchemaType schemaType, SchemaType baseType) {
        if (schemaType == null) {
            return false;
        }
        return schemaType.equals(baseType) ? true : isInstanceOf(schemaType.getBaseType(), baseType);
    }

    public static boolean isBinaryType(SchemaType schemaType) {
        return isInstanceOf(schemaType, XmlHexBinary.type) || isInstanceOf(schemaType, XmlBase64Binary.type);
    }

    public static String getDocumentation(SchemaType schemaType) {
        String result = null;
        String xsPrefix = null;

        SchemaField containerField = schemaType.getContainerField();

        if (containerField instanceof SchemaLocalElement) {
            SchemaAnnotation annotation = ((SchemaLocalElement) containerField).getAnnotation();
            if (annotation != null) {
                XmlObject[] userInformation = annotation.getUserInformation();
                if (userInformation != null && userInformation.length > 0) {
                    XmlObject xmlObject = userInformation[0];
                    XmlCursor cursor = xmlObject.newCursor();
                    xsPrefix = cursor.prefixForNamespace("http://www.w3.org/2001/XMLSchema");
                    cursor.dispose();

                    result = xmlObject.xmlText(); // XmlUtils.getElementText( (
                    // Element )
                    // userInformation[0].getDomNode());
                }
            }
        }

        if (result == null && schemaType != null && schemaType.getAnnotation() != null) {
            XmlObject[] userInformation = schemaType.getAnnotation().getUserInformation();
            if (userInformation != null && userInformation.length > 0 && userInformation[0] != null) {
                XmlObject xmlObject = userInformation[0];
                XmlCursor cursor = xmlObject.newCursor();
                xsPrefix = cursor.prefixForNamespace("http://www.w3.org/2001/XMLSchema");
                cursor.dispose();
                result = xmlObject.xmlText(); // = XmlUtils.getElementText( (
                // Element )
                // userInformation[0].getDomNode());
            }
        }

        if (result != null) {
            result = result.trim();
            if (result.startsWith("<") && result.endsWith(">")) {
                int ix = result.indexOf('>');
                if (ix > 0) {
                    result = result.substring(ix + 1);
                }

                ix = result.lastIndexOf('<');
                if (ix >= 0) {
                    result = result.substring(0, ix);
                }
            }

            if (xsPrefix == null || xsPrefix.length() == 0) {
                xsPrefix = "xs:";
            } else {
                xsPrefix += ":";
            }

            // result = result.trim().replaceAll( "<" + xsPrefix + "br/>", "\n"
            // ).trim();
            result = result.trim().replaceAll(xsPrefix, "").trim();

            result = StringUtils.toHtml(result);
        }

        return result;
    }

    public static String[] getEnumerationValues(SchemaType schemaType, boolean addNull) {
        if (schemaType != null) {
            XmlAnySimpleType[] enumerationValues = schemaType.getEnumerationValues();
            if (enumerationValues != null && enumerationValues.length > 0) {
                if (addNull) {
                    String[] values = new String[enumerationValues.length + 1];
                    values[0] = null;

                    for (int c = 1; c < values.length; c++) {
                        values[c] = enumerationValues[c - 1].getStringValue();
                    }

                    return values;
                } else {
                    String[] values = new String[enumerationValues.length];

                    for (int c = 0; c < values.length; c++) {
                        values[c] = enumerationValues[c].getStringValue();
                    }

                    return values;
                }
            }
        }

        return new String[0];
    }

    public static Collection<? extends QName> getExcludedTypes() {
        String excluded = SoapUI.getSettings().getString(WsdlSettings.EXCLUDED_TYPES, null);
        return SettingUtils.string2QNames(excluded);
    }

    public static boolean isAnyType(SchemaType schemaType) {
        return schemaType != null
                && (schemaType.getBuiltinTypeCode() == SchemaType.BTC_ANY_TYPE || (schemaType.getBaseType() != null && schemaType
                .getBaseType().getBuiltinTypeCode() == SchemaType.BTC_ANY_TYPE));
    }
}
